package scala.scalanative
package junit

// Ported from Scala.js

import sbt.testing._

import org.junit._

private[junit] final class Reporter(
    eventHandler: EventHandler,
    loggers: Array[Logger],
    settings: RunSettings,
    taskDef: TaskDef
) {

  def reportRunStarted(): Unit =
    log(
      infoOrDebug(RunSettings.Verbosity.Started),
      Ansi.c("Test run ", Ansi.BLUE) +
        formatClass(taskDef.fullyQualifiedName(), Ansi.YELLOW) +
        Ansi.c(" started", Ansi.BLUE)
    )

  def reportRunFinished(
      failed: Int,
      ignored: Int,
      total: Int,
      timeInSeconds: Double
  ): Unit = {
    val msg = {
      Ansi.c("Test run ", Ansi.BLUE) +
        formatClass(taskDef.fullyQualifiedName(), Ansi.YELLOW) +
        Ansi.c(" finished: ", Ansi.BLUE) +
        Ansi.c(s"$failed failed", if (failed == 0) Ansi.BLUE else Ansi.RED) +
        Ansi.c(s", ", Ansi.BLUE) +
        Ansi.c(
          s"$ignored ignored",
          if (ignored == 0) Ansi.BLUE else Ansi.YELLOW
        ) +
        Ansi.c(f", $total total, $timeInSeconds%.3fs", Ansi.BLUE)
    }

    log(infoOrDebug(RunSettings.Verbosity.RunFinished), msg)
  }

  def reportIgnored(method: Option[String]): Unit = {
    logTestInfo(_.info, method, "ignored")
    emitEvent(method, Status.Ignored, 0, None)
  }

  def reportTestStarted(method: String): Unit =
    logTestInfo(
      infoOrDebug(RunSettings.Verbosity.Started),
      Some(method),
      "started"
    )

  def reportTestFinished(
      method: String,
      succeeded: Boolean,
      timeInSeconds: Double
  ): Unit = {
    logTestInfo(
      infoOrDebug(RunSettings.Verbosity.TestFinished),
      Some(method),
      f"finished, took $timeInSeconds%.3f sec"
    )

    if (succeeded)
      emitEvent(Some(method), Status.Success, timeInSeconds, None)
  }

  def reportErrors(
      prefix: String,
      method: Option[String],
      timeInSeconds: Double,
      errors: List[Throwable]
  ): Unit = {
    def emit(t: Throwable): Unit = {
      logTestException(_.error, prefix, method, t, timeInSeconds)
      trace(t)
    }

    if (errors.nonEmpty) {
      emit(errors.head)
      emitEvent(method, Status.Failure, timeInSeconds, Some(errors.head))
      errors.tail.foreach(emit)
    }
  }

  def reportAssumptionViolation(
      method: Option[String],
      timeInSeconds: Double,
      e: Throwable
  ): Unit = {
    logTestException(
      _.warn,
      "Test assumption in test ",
      method,
      e,
      timeInSeconds
    )
    emitEvent(method, Status.Skipped, timeInSeconds, Some(e))
  }

  private def logTestInfo(
      level: Reporter.Level,
      method: Option[String],
      msg: String
  ): Unit =
    log(level, s"Test ${formatTest(method, Ansi.CYAN)} $msg")

  private def logTestException(
      level: Reporter.Level,
      prefix: String,
      method: Option[String],
      ex: Throwable,
      timeInSeconds: Double
  ): Unit = {
    val fmtName =
      if (!settings.logExceptionClass ||
          !settings.logAssert && ex.isInstanceOf[AssertionError]) ""
      else formatClass(ex.getClass.getName, Ansi.RED) + ": "

    val m = formatTest(method, Ansi.RED)
    val msg =
      f"$prefix$m failed: $fmtName${ex.getMessage}, took $timeInSeconds%.3f sec"
    log(level, msg)
  }

  private def trace(t: Throwable): Unit = {
    if (!t.isInstanceOf[AssertionError] || settings.logAssert) {
      logTrace(t)
    }
  }

  private def infoOrDebug(atVerbosity: RunSettings.Verbosity): Reporter.Level =
    if (atVerbosity.ordinal <= settings.verbosity.ordinal) _.info
    else _.debug

  private def formatTest(method: Option[String], color: String): String = {
    method.fold(formattedTestClass) { method =>
      val fmtMethod = Ansi.c(settings.decodeName(method), color)
      s"$formattedTestClass.$fmtMethod"
    }
  }

  private lazy val formattedTestClass =
    formatClass(taskDef.fullyQualifiedName(), Ansi.YELLOW)

  private def formatClass(fullName: String, color: String): String = {
    val (prefix, name) = fullName.splitAt(fullName.lastIndexOf(".") + 1)
    prefix + Ansi.c(name, color)
  }

  private def emitEvent(
      method: Option[String],
      status: Status,
      timeInSeconds: Double,
      throwable: Option[Throwable]
  ): Unit = {
    val testName = method.fold(taskDef.fullyQualifiedName())(method =>
      taskDef.fullyQualifiedName() + "." + settings.decodeName(method)
    )
    val selector = new TestSelector(testName)
    val optionalThrowable: OptionalThrowable = new OptionalThrowable(
      throwable.orNull
    )
    val duration: Long = (timeInSeconds * 1000).toLong
    eventHandler.handle(
      new JUnitEvent(taskDef, status, selector, optionalThrowable, duration)
    )
  }

  def log(level: Reporter.Level, s: String): Unit = {
    for (l <- loggers)
      level(l)(filterAnsiIfNeeded(l, s))
  }

  private def filterAnsiIfNeeded(l: Logger, s: String): String =
    if (l.ansiCodesSupported() && settings.color) s
    else Ansi.filterAnsi(s)

  private def logTrace(t: Throwable): Unit = {
    val trace = t.getStackTrace
      .dropWhile { p =>
        val clsName = p.getClassName()
        clsName != null && {
          clsName.startsWith("java.lang.Throwable") ||
          clsName.startsWith("scala.scalanative.runtime.StackTrace") ||
          clsName.startsWith("scala.scalanative.runtime.Throwable")
        }
      }
    val testFileName = {
      if (settings.color) findTestFileName(trace)
      else null
    }
    val i = trace.indexWhere { p =>
      p.getClassName() != null &&
      p.getClassName().startsWith("scala.scalanative.junit.JUnitTask")
    } - 1
    val m = if (i > 0) i else trace.length - 1
    logStackTracePart(trace, m, trace.length - m - 1, t, testFileName)
  }

  private def logStackTracePart(
      trace: Array[StackTraceElement],
      m: Int,
      framesInCommon: Int,
      t: Throwable,
      testFileName: String
  ): Unit = {
    def isJunit(i: Int): Boolean = {
      val cls = trace(i).getClassName
      cls.startsWith("org.junit.") || cls.startsWith("org.hamcrest.")
    }

    def isReflect(i: Int): Boolean = {
      val cls = trace(i).getClassName
      cls.startsWith("java.lang.reflect.") || cls.startsWith("sun.reflect.")
    }

    var head = 0
    while (head <= m && isJunit(head)) head += 1

    var last = head
    while (last < m && !isJunit(last + 1)) last += 1

    while (last > head && isReflect(last)) last -= 1

    for (i <- head to last) {
      val msg = "    at " + stackTraceElementToString(trace(i), testFileName)
      log(_.error, msg)
    }
    if (m > last) {
      // skip junit-related frames
      log(_.error, "    ...")
    } else if (framesInCommon != 0) {
      // skip frames that were in the previous trace too
      log(_.error, "    ... " + framesInCommon + " more")
    }
    logStackTraceAsCause(trace, t.getCause(), testFileName)
  }

  private def logStackTraceAsCause(
      causedTrace: Array[StackTraceElement],
      t: Throwable,
      testFileName: String
  ): Unit = {
    if (t != null) {
      val trace = t.getStackTrace
      var m = trace.length - 1
      var n = causedTrace.length - 1
      while (m >= 0 && n >= 0 && trace(m) == causedTrace(n)) {
        m -= 1
        n -= 1
      }
      log(_.error, "Caused by: " + t)
      logStackTracePart(trace, m, trace.length - 1 - m, t, testFileName)
    }
  }

  private def findTestFileName(trace: Array[StackTraceElement]): String =
    trace
      .find(_.getClassName == taskDef.fullyQualifiedName())
      .map(_.getFileName)
      .orNull

  private def stackTraceElementToString(
      e: StackTraceElement,
      testFileName: String
  ): String = {
    val highlight = settings.color && {
      taskDef.fullyQualifiedName() == e.getClassName ||
      (testFileName != null && testFileName == e.getFileName)
    }
    var r = ""
    r += settings.decodeName(e.getClassName + '.' + e.getMethodName)
    r += '('

    if (e.isNativeMethod) {
      r += Ansi.c("Native Method", if (highlight) Ansi.YELLOW else null)
    } else if (e.getFileName == null) {
      r += Ansi.c("Unknown Source", if (highlight) Ansi.YELLOW else null)
    } else {
      r += Ansi.c(e.getFileName, if (highlight) Ansi.MAGENTA else null)
      if (e.getLineNumber >= 0) {
        r += ':'
        r += Ansi.c(
          String.valueOf(e.getLineNumber),
          if (highlight) Ansi.YELLOW else null
        )
      }
    }
    r += ')'
    r
  }
}

private[junit] object Reporter {
  type Level = Logger => String => Unit
}
